/*
NPlot - A charting library for .NET

CandlePlot.cs
Copyright (C) 2003
Matt Howlett
Pawel Konieczny 

Redistribution and use of NPlot or parts there-of in source and
binary forms, with or without modification, are permitted provided
that the following conditions are met:

1. Re-distributions in source form must retain at the head of each
   source file the above copyright notice, this list of conditions
   and the following disclaimer.

2. Any product ("the product") that makes use NPlot or parts 
   there-of must either:
  
    (a) allow any user of the product to obtain a complete machine-
        readable copy of the corresponding source code for the 
        product and the version of NPlot used for a charge no more
        than your cost of physically performing source distribution,
	on a medium customarily used for software interchange, or:

    (b) reproduce the following text in the documentation, about 
        box or other materials intended to be read by human users
        of the product that is provided to every human user of the
        product: 
   
              "This product includes software developed as 
              part of the NPlot library project available 
              from: http://www.nplot.com/" 

        The words "This product" may optionally be replace with 
        the actual name of the product.

------------------------------------------------------------------------

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

using System;
using System.Drawing;
using System.Data;

namespace NPlot
{

    /// <summary>
	/// Encapsulates open, low, high and close values useful for specifying financial data
	/// over a time period, together with a [single] x-value indicating the time [period] the
	/// data corresponds to. 
	/// </summary>
	public class PointOLHC
	{

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="x">value representing the time period that the financial values refer to</param>
		/// <param name="open">The value at open of time period.</param>
		/// <param name="low">The low value over the time period</param>
		/// <param name="high">The high value over the time period.</param>
		/// <param name="close">The value at close of time period.</param>
		public PointOLHC( double x, double open, double low, double high, double close )
		{
			this.x_ = x;
			this.open_ = open;
			this.close_ = close;
			this.low_ = low;
			this.high_ = high;
		}


		/// <summary>
		/// value representing the time period that the financial values apply to.
		/// </summary>
		public double X
		{
			get
			{
				return x_;
			}
			set
			{
				this.x_ = value;
			}
		}
		private double x_;


		/// <summary>
		/// The value at open of time period.
		/// </summary>
		public double Open
		{
			get
			{
				return open_;
			}
			set
			{
				open_ = value;
			}
		}
		private double open_;


		/// <summary>
		/// The value at close of time period.
		/// </summary>
		public double Close
		{
			get
			{
				return close_;
			}
			set
			{
				close_ = value;
			}
		}
		private double close_;


		/// <summary>
		/// Low value of the time period.
		/// </summary>
		public double Low
		{
			get
			{
				return low_;
			}
			set
			{
				low_ = value;
			}
		}
		private double low_;


		/// <summary>
		/// High value of the time period.
		/// </summary>
		public double High
		{
			get
			{
				return high_;
			}
			set
			{
				high_ = value;
			}
		}
		private double high_;

	}


	/// <summary>
	/// Encapsulates functionality for drawing finacial candle charts. 
	/// </summary>
	public class CandlePlot : BasePlot, IPlot
	{

        /// <summary>
        /// 
        /// </summary>
        public abstract class CandleStyle
        {
            /// <summary>
            /// 
            /// </summary>
            /// <param name="d"></param>
            /// <returns></returns>
            public abstract CandleStyle Create(CandleDataAdapter d);
        }

        /// <summary>
        /// 
        /// </summary>
        public class Stick : CandleStyle
        {
            private Stick() { }

            /// <summary>
            /// 
            /// </summary>
            /// <param name="d"></param>
            /// <returns></returns>
            public override CandleStyle Create(CandleDataAdapter d)
            {
                return new Stick();
            }
        }
        

        /// <summary>
        /// This class is responsible for interpreting the various ways you can 
	    /// specify data to CandlePlot objects
        /// </summary>
        public class CandleDataAdapter
		{
			private object openData_;
			private object lowData_;
			private object highData_;
			private object closeData_;
			private object abscissaData_;

			private object dataSource_;
			private string dataMember_;
			DataRowCollection rows_ = null;

			// speed optimizations if data is double.
			private double[] openDataArray_;
			private double[] lowDataArray_;
			private double[] highDataArray_;
			private double[] closeDataArray_;
			private double[] abscissaDataArray_;
			private bool useDoublesArrays_;
            

			/// <summary>
            /// Constructor
            /// </summary>
            /// <param name="dataSource"></param>
            /// <param name="dataMember"></param>
            /// <param name="abscissaData"></param>
            /// <param name="openData"></param>
            /// <param name="lowData"></param>
            /// <param name="highData"></param>
            /// <param name="closeData"></param>
			public CandleDataAdapter( 
				object dataSource, string dataMember, object abscissaData,
				object openData, object lowData, object highData, object closeData )
			{
				this.openData_ = openData;
				this.lowData_ = lowData;
				this.highData_ = highData;
				this.closeData_ = closeData;
				this.abscissaData_ = abscissaData;

				this.dataSource_ = dataSource;
				this.dataMember_ = dataMember;

				if (dataSource_ != null)
				{
					if ( dataSource_ is DataSet )
					{
						if (dataMember_ != null)
						{
							rows_ = ((DataTable)((DataSet)dataSource_).Tables[dataMember_]).Rows;
						}
						else
						{
							rows_ = ((DataTable)((DataSet)dataSource_).Tables[0]).Rows;
						}
					}

					else if (dataSource_ is DataTable )
					{
						rows_ = ((DataTable)dataSource_).Rows;
					}

					else
					{
						throw new NPlotException ( "not implemented yet" );
					}
				}

				openDataArray_ = openData_ as System.Double[];
				lowDataArray_ = lowData_ as System.Double[];
				highDataArray_ = highData_ as System.Double[];
				closeDataArray_ = closeData_ as System.Double[];
				abscissaDataArray_ = abscissaData_ as System.Double[];
				useDoublesArrays_ = (
					openDataArray_ != null &&
					lowDataArray_ != null &&
					highDataArray_ != null &&
					lowDataArray_ != null &&
					abscissaDataArray_ != null
				);

			}


            /// <summary>
            /// Gets the ith point in the candle adapter
            /// </summary>
            /// <param name="i">index of datapoint to get</param>
            /// <returns>the datapoint.</returns>
			public PointOLHC this[int i]
			{
				get
				{
                    
					// try a fast track first
                    if (useDoublesArrays_)
                    {
                        return new PointOLHC(
                            abscissaDataArray_[i],
                            openDataArray_[i],
                            lowDataArray_[i],
                            highDataArray_[i],
                            closeDataArray_[i]);
                    }

                    // is the data coming from a data source?
                    else if (rows_ != null)
                    {
                        double x = Utils.ToDouble(((DataRow)(rows_[i]))[(string)abscissaData_]);
                        double open = Utils.ToDouble(((DataRow)(rows_[i]))[(string)openData_]);
                        double low = Utils.ToDouble(((DataRow)(rows_[i]))[(string)lowData_]);
                        double high = Utils.ToDouble(((DataRow)(rows_[i]))[(string)highData_]);
                        double close = Utils.ToDouble(((DataRow)(rows_[i]))[(string)closeData_]);

                        return new PointOLHC(x, open, low, high, close);
                    }

					// the data is coming from individual arrays.
                    else if (abscissaData_ is Array && openData_ is Array && lowData_ is Array && highData_ is Array && closeData_ is Array)
                    {
                        double x = Utils.ToDouble(((Array)abscissaData_).GetValue(i));
                        double open = Utils.ToDouble(((Array)openData_).GetValue(i));
                        double low = Utils.ToDouble(((Array)lowData_).GetValue(i));
                        double high = Utils.ToDouble(((Array)highData_).GetValue(i));
                        double close = Utils.ToDouble(((Array)closeData_).GetValue(i));

                        return new PointOLHC(x, open, low, high, close);
                    }

                    else
                    {
                        throw new NPlotException("not implemented yet");
                    }

                }
			}


            /// <summary>
            /// The number of datapoints available via the candle adapter.
            /// </summary>
            /// <value>the number of datapoints available.</value>
			public int Count
			{
				get
				{
					// this is inefficient [could set up delegates in constructor].

					if (useDoublesArrays_)
					{
						return openDataArray_.Length;
					}

					if (openData_ == null)
					{
						return 0;
					}

					if (rows_ != null)
					{
						return rows_.Count;
					}

                    if (openData_ is Array)
                    {
                        int size = ((Array)openData_).Length;
                        if (size != ((Array)closeData_).Length)
                            throw new NPlotException("open and close arrays are not of same length");
                        if (size != ((Array)lowData_).Length)
                            throw new NPlotException("open and close arrays are not of same length");
                        if (size != ((Array)highData_).Length)
                            throw new NPlotException("open and close arrays are not of same length");
                        return size;
                    }

                    throw new NPlotException( "data not in correct format" );
				}
			}


			/// <summary>
			/// Returns an x-axis that is suitable for drawing the data.
			/// </summary>
			/// <returns>A suitable x-axis.</returns>
			public Axis SuggestXAxis()
			{
				double min;
				double max;
				double minStep = 0.0;

                if (this.rows_ == null)
                {
                    Utils.ArrayMinMax((System.Collections.IList)this.abscissaData_, out min, out max);

                    if (((System.Collections.IList)abscissaData_).Count > 1)
                    {
                        double first = Utils.ToDouble(((Array)abscissaData_).GetValue(0));
                        double second = Utils.ToDouble(((Array)abscissaData_).GetValue(1));
                        minStep = Math.Abs(second - first);
                    }
                    if (((System.Collections.IList)abscissaData_).Count > 2)
                    {
                        double first = Utils.ToDouble(((Array)abscissaData_).GetValue(1));
                        double second = Utils.ToDouble(((Array)abscissaData_).GetValue(2));
                        if (Math.Abs(second - first) < minStep)
                            minStep = Math.Abs(second - first);
                    }
                    if (((System.Collections.IList)abscissaData_)[0] is DateTime)
                    {
                        return new DateTimeAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                    else
                    {
                        return new LinearAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                }
                else
                {

                    Utils.RowArrayMinMax(this.rows_, out min, out max, (string)this.abscissaData_);

                    if (rows_.Count > 1)
                    {
                        double first = Utils.ToDouble(rows_[0][(string)abscissaData_]);
                        double second = Utils.ToDouble(rows_[1][(string)abscissaData_]);
                        minStep = Math.Abs(second - first);
                    }
                    if (rows_.Count > 2)
                    {
                        double first = Utils.ToDouble(rows_[1][(string)abscissaData_]);
                        double second = Utils.ToDouble(rows_[2][(string)abscissaData_]);
                        if (Math.Abs(second - first) < minStep)
                            minStep = Math.Abs(second - first);
                    }

                    if ((rows_[0])[(string)abscissaData_] is DateTime)
                    {
                        return new DateTimeAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                    else
                    {
                        return new LinearAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                }

            }


			/// <summary>
			/// Returns a y-axis that is suitable for drawing the data.
			/// </summary>
			/// <returns>A suitable y-axis.</returns>
			public Axis SuggestYAxis()
			{

				double min_l;
				double max_l;
				double min_h;
				double max_h;

                if (this.rows_ == null)
                {
                    Utils.ArrayMinMax((System.Collections.IList)lowData_, out min_l, out max_l);
                    Utils.ArrayMinMax((System.Collections.IList)highData_, out min_h, out max_h);
                }
                else
                {
                    Utils.RowArrayMinMax(this.rows_, out min_l, out max_l, (string)this.lowData_);
                    Utils.RowArrayMinMax(this.rows_, out min_h, out max_h, (string)this.highData_);
                }

                Axis a = new LinearAxis( min_l, max_h );
				a.IncreaseRange( 0.08 );
				return a;
			}

		}

		
		/// <summary>
		/// Default constructor.
		/// </summary>
		public CandlePlot()
		{
		}


        /// <summary>
        /// Calculates the physical (not world) separation between abscissa values.
        /// </summary>
        /// <param name="cd">Candle adapter containing data</param>
        /// <param name="xAxis">Physical x axis the data is plotted against.</param>
        /// <returns>physical separation between abscissa values.</returns>
		private static float CalculatePhysicalSeparation( CandleDataAdapter cd, PhysicalAxis xAxis )
		{
			if (cd.Count > 1)
			{
				int xPos1 = (int)(xAxis.WorldToPhysical( ((PointOLHC)cd[0]).X, false )).X;
				int xPos2 = (int)(xAxis.WorldToPhysical( ((PointOLHC)cd[1]).X, false )).X;		
				int minDist = xPos2 - xPos1;

				if (cd.Count > 2)
				{  // to be pretty sure we get the smallest gap.
					int xPos3 = (int)(xAxis.WorldToPhysical(((PointOLHC)cd[2]).X, false)).X;
					if (xPos3 - xPos2 < minDist) minDist = xPos3 - xPos2;

					if (cd.Count > 3)
					{
						int xPos4 = (int)(xAxis.WorldToPhysical(((PointOLHC)cd[3]).X, false)).X;
						if (xPos4 - xPos3 < minDist) minDist = xPos4 - xPos3;
					}
				}

				return minDist;
			}

            return 0.0f;
        }


		/// <summary>
		/// Draws the candle plot on a GDI+ surface agains the provided x and y axes.
		/// </summary>
		/// <param name="g">The GDI+ surface on which to draw.</param>
		/// <param name="xAxis">The X-Axis to draw against.</param>
		/// <param name="yAxis">The Y-Axis to draw against.</param>
		public void Draw( Graphics g, PhysicalAxis xAxis, PhysicalAxis yAxis )
		{
			CandleDataAdapter cd = new CandleDataAdapter( this.DataSource, this.DataMember, 
				this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData );

			Brush bearishBrush = new SolidBrush( BearishColor );
			Brush bullishBrush = new SolidBrush( BullishColor );

            uint offset = 0;
            if (this.centered_)
            {
                offset = (uint)(CalculatePhysicalSeparation(cd,xAxis) / 2.0f);
            }

            uint addAmount = (uint)StickWidth/2;
			uint stickWidth = (uint)StickWidth;

			if (StickWidth == AutoScaleStickWidth)
			{
				// default
				addAmount = 2;
				stickWidth = 4;
            
                float minDist = CalculatePhysicalSeparation( cd, xAxis );

                addAmount = (uint)(minDist / 3);
                stickWidth = addAmount * 2;
            }

			Pen	p =	new	Pen(this.color_);

			/*
			// brant hyatt proposed.
			if (this.Style == Styles.Stick)
			{
				p.Width = stickWidth;
 				addAmount = stickWidth + 2;
			}
			*/

			for (int i=0; i<cd.Count; ++i)
			{

				PointOLHC point = (PointOLHC)cd[i];
				if ( (!double.IsNaN (point.Open)) && (!double.IsNaN(point.High)) && (!double.IsNaN (point.Low)) && (!double.IsNaN(point.Close)) )
				{

					int xPos = (int)(xAxis.WorldToPhysical( point.X, false )).X;

					if (xPos + offset + addAmount < xAxis.PhysicalMin.X || xAxis.PhysicalMax.X < xPos + offset - addAmount)
						continue;

					int yPos1 = (int)(yAxis.WorldToPhysical( point.Low, false )).Y;
					int yPos2 = (int)(yAxis.WorldToPhysical( point.High, false )).Y;
					int yPos3 = (int)(yAxis.WorldToPhysical( point.Open, false )).Y;
					int yPos4 = (int)(yAxis.WorldToPhysical( point.Close, false )).Y;

					if (this.Style == Styles.Stick)
					{

						/*
						// brant hyatt proposed.
						if (i > 0) 
						{
							if ( ((PointOLHC)cd[i]).Close > ((PointOLHC)cd[i-1]).Close)
							{
								p.Color = BullishColor;
							}
							else
							{
								p.Color = BearishColor;
							}
						}
						*/

						g.DrawLine( p, xPos+offset, yPos1, xPos+offset, yPos2 );
						g.DrawLine( p, xPos-addAmount+offset, yPos3, xPos+offset, yPos3 );
						g.DrawLine( p, xPos+offset, yPos4, xPos+addAmount+offset, yPos4 );
					}

					else if (this.Style == Styles.Filled)
					{
						g.DrawLine( p, xPos+offset, yPos1, xPos+offset, yPos2 );
						if (yPos3 > yPos4)
						{
							g.FillRectangle( bullishBrush, xPos-addAmount+offset, yPos4, stickWidth, yPos3 - yPos4 );
							g.DrawRectangle( p, xPos-addAmount+offset, yPos4, stickWidth, yPos3 - yPos4 );
						}
						else if (yPos3 < yPos4)
						{
							g.FillRectangle( bearishBrush, xPos-addAmount+offset, yPos3, stickWidth, yPos4 - yPos3 );
							g.DrawRectangle( p, xPos-addAmount+offset, yPos3, stickWidth, yPos4 - yPos3 );
						}
						else
						{
							g.DrawLine( p, xPos-addAmount+offset, yPos3, xPos-addAmount+stickWidth+offset, yPos3 );
						}

					}

				}
			}

		}


		/// <summary>
		/// Returns an x-axis that is suitable for drawing this plot.
		/// </summary>
		/// <returns>A suitable x-axis.</returns>
		public Axis SuggestXAxis()
		{
			CandleDataAdapter candleData = new CandleDataAdapter( this.DataSource, this.DataMember, 
				this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData );
		
			return candleData.SuggestXAxis();
		}


		/// <summary>
		/// Returns a y-axis that is suitable for drawing this plot.
		/// </summary>
		/// <returns>A suitable y-axis.</returns>
		public Axis SuggestYAxis()
		{
			CandleDataAdapter candleData = new CandleDataAdapter( this.DataSource, this.DataMember, 
				this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData );

			return candleData.SuggestYAxis();
		}


		/// <summary>
		/// Gets or sets the data, or column name for the open values.
		/// </summary>
		public object OpenData
		{
			get
			{
				return openData_;
			}
			set
			{
				openData_ = value;
			}
		}
		private object openData_ = null;


		/// <summary>
		/// Gets or sets the data, or column name for the interval low values.
		/// </summary>
		public object LowData
		{
			get
			{
				return lowData_;
			}
			set
			{
				lowData_ = value;
			}
		}
		private object lowData_ = null;


		/// <summary>
		/// Gets or sets the data, or column name for the interval high values.
		/// </summary>
		public object HighData
		{
			get
			{
				return highData_;
			}
			set
			{
				highData_ = value;
			}
		}
		private object highData_ = null;


		/// <summary>
		/// Gets or sets the data, or column name for the close values.
		/// </summary>
		public object CloseData
		{
			get
			{
				return closeData_;
			}
			set
			{
				closeData_ = value;
			}
		}	
		private object closeData_ = null;


		/// <summary>
		/// Gets or sets the data, or column name for the abscissa [x] axis.
		/// </summary>
		public object AbscissaData
		{
			get
			{
				return abscissaData_;
			}
			set
			{
				abscissaData_ = value;
			}
		}
		private object abscissaData_ = null;


		/// <summary>
		/// Draws a representation of this plot in the legend.
		/// </summary>
		/// <param name="g">The graphics surface on which to draw.</param>
		/// <param name="startEnd">A rectangle specifying the bounds of the area in the legend set aside for drawing.</param>
		public virtual void DrawInLegend( Graphics g, Rectangle startEnd )
		{
			Pen	p =	new	Pen(this.color_);

			g.DrawLine( p, startEnd.Left, (startEnd.Top + startEnd.Bottom)/2, 
				startEnd.Right, (startEnd.Top + startEnd.Bottom)/2 );

		}


		/// <summary>
		/// Color of this plot [excluding interior of filled boxes if Style is fill]. To
		/// change the Bullish and Bearish colours in Filled mode, use the BullishColor
		/// and BearishColor properties.
		/// </summary>
		public System.Drawing.Color Color
		{
			get
			{
				return color_;
			}
			set
			{
				color_ = value;
			}
		}
		Color color_ = Color.Black;
		

		/// <summary>
		/// Possible CandleStick styles.
		/// </summary>
		public enum Styles
		{

			/// <summary>
			/// Draw vertical line between low and high, tick on left for open and tick on right for close.
			/// </summary>
			Stick,

			/// <summary>
			/// Draw vertical line between low and high and place on top of this a box with bottom
			/// and top determined by open and high values. The box is filled using the colors specified
			/// in BullishColor and BearishColor properties.
			/// </summary>
			Filled

		}


		/// <summary>
		/// Specifies the CandleStick style to use.
		/// </summary>
		public Styles Style = Styles.Filled;


		/// <summary>
		/// If CandlePlot.Style is Filled, then bullish open-close moves are displayed in this color.
		/// </summary>
		public Color BullishColor = Color.White;


		/// <summary>
		/// If CandlePlot.Style is Filled, then bearish moves are displayed in this color.
		/// </summary>
		public Color BearishColor = Color.Black;


		/// <summary>
		/// Width of each stick in pixels. It is best if this is an odd number.
		/// </summary>
		public int StickWidth
		{
			get
			{
				return stickWidth_;
			}
			set
			{
				if (value < 1)
				{
					throw new NPlotException( "Stick width must be greater than 0." );
				}
				stickWidth_ = value;
			}
		}
		private int stickWidth_ = AutoScaleStickWidth;


		/// <summary>
		/// If stick width is set equal to this value, the width will be 
		/// automatically scaled dependant on the space between sticks.
		/// </summary>
		public const int AutoScaleStickWidth = 0;

        /// <summary>
        /// If true (default), bars will be centered on the abscissa times. 
        /// If false, bars will be drawn between the corresponding abscissa time
        /// and the next abscissa time. 
        /// </summary>
        /// <value></value>
        public bool Centered
        {
            get
            {
                return centered_;
            }
            set
            {
                centered_ = value;
            }
        }
        private bool centered_ = true;


        /// <summary>
		/// Write data associated with the plot as text.
		/// </summary>
		/// <param name="sb">the string builder to write to.</param>
		/// <param name="region">Only write out data in this region if onlyInRegion is true.</param>
		/// <param name="onlyInRegion">If true, only data in region is written, else all data is written.</param>
		/// <remarks>TODO: not implemented.</remarks>
		public void WriteData( System.Text.StringBuilder sb, RectangleD region, bool onlyInRegion )
		{
		}

	}
}
